﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using UnityEngine;
using UnityEngine.Profiling;
using Debug = UnityEngine.Debug;

namespace PerfAssist.CoroutineTracker
{

    public class CoroutineRuntimeTrackingConfig
    {
        // enable the whole tracking system
        public static bool EnableTracking = false;

        // last n seconds are kept
        public static float BroadcastInterval = 0.5f;
    }

    public class CoroutineNameCache
    {
        private static readonly Dictionary<string, string> _mangledNames = new Dictionary<string, string>();

        public static string Mangle(string rawName)
        {
            string mangled;
            if (_mangledNames.TryGetValue(rawName, out mangled))
                return mangled;

            // (manual-mangling) which provides better readability
            mangled = rawName.Replace('<', '{').Replace('>', '}');

            // (auto-mangling) Url Escaping 
            //mangled = System.Uri.EscapeDataString(rawName);

            _mangledNames[rawName] = mangled;
            return mangled;
        }
    }

    public class TrackedCoroutine : IEnumerator
    {
        private static Stopwatch _stopWatch;

        private static int _seqNext;
        private readonly string _mangledName;
        private readonly IEnumerator _routine;
        private readonly int _seqID;

        public TrackedCoroutine(IEnumerator routine)
        {
            _routine = routine;
            _mangledName = CoroutineNameCache.Mangle(_routine.GetType().ToString());
            _seqID = _seqNext++;

            RuntimeCoroutineStats.Instance.MarkCreation(_seqID, _mangledName);
        }

        object IEnumerator.Current
        {
            get { return _routine.Current; }
        }

        public bool MoveNext()
        {
            Profiler.BeginSample(_mangledName);

            if (_stopWatch == null)
                _stopWatch = Stopwatch.StartNew();

            _stopWatch.Reset();
            _stopWatch.Start();

            var next = _routine.MoveNext();

            _stopWatch.Stop();
            Profiler.EndSample();

            var timeConsumed = (float) (_stopWatch.ElapsedTicks / (double) Stopwatch.Frequency);
            RuntimeCoroutineStats.Instance.MarkMoveNext(_seqID, timeConsumed);

            if (!next)
                RuntimeCoroutineStats.Instance.MarkTermination(_seqID);

            return next;
        }

        public void Reset()
        {
            _routine.Reset();
        }
    }

    public class RuntimeCoroutineTracker
    {
        public static Coroutine InvokeStart(MonoBehaviour initiator, IEnumerator routine)
        {
            if (!CoroutineRuntimeTrackingConfig.EnableTracking)
                return initiator.StartCoroutine(routine);

            try
            {
                return initiator.StartCoroutine(new TrackedCoroutine(routine));
            }
            catch (Exception ex)
            {
                Debug.LogException(ex);
                return null;
            }
        }

        public static Coroutine InvokeStart(MonoBehaviour initiator, string methodName, object arg = null)
        {
            if (!CoroutineRuntimeTrackingConfig.EnableTracking)
                return initiator.StartCoroutine(methodName, arg);

            try
            {
                var type = initiator.GetType();
                if (type == null)
                    throw new ArgumentNullException("initiator", "invalid initiator (null type)");

                var coroutineMethod = type.GetMethod(methodName,
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
                if (coroutineMethod == null)
                    throw new ArgumentNullException("methodName",
                        string.Format("Invalid method {0} (method not found)", methodName));

                object[] args = null;
                if (arg != null) args = new object[1] {arg};

                var coroutineEnumerator = coroutineMethod.Invoke(initiator, args) as IEnumerator;
                if (coroutineEnumerator == null)
                    throw new ArgumentNullException("methodName",
                        string.Format("Invalid method {0} (not an IEnumerator)", methodName));

                return InvokeStart(initiator, coroutineEnumerator);
            }
            catch (Exception ex)
            {
                Debug.LogException(ex);
                return null;
            }
        }
    }
}